import axios, { AxiosError, AxiosInstance, AxiosResponse } from "axios";
import { navigate } from "../actions/Router";
import { NulException, NulExceptionConstants } from "./NulException";
import { int } from "../types";
import * as TypeHelper from "./TypeHelper";
import { postGlobalAlert } from "../actions/GlobalAlert";
import { clearAuth } from "./AuthHelper";

interface NulResponse<T> {
  version: number;
  code: NulExceptionConstants;
  message: string;
  result: T;
}

type ErrorHandler = (code: int, message: string) => void;
type ErrorHandlerMap = { [key: int]: ErrorHandler };

const fallbackHandler: ErrorHandler = (code, message) => {
  postGlobalAlert('Error', `${message} (Code ${code})`, 'warning');
};

function createAxiosInstance<Response>(withToken: boolean, errorHandlers?: ErrorHandlerMap, defaultHandler?: ErrorHandler) {
  const instance: AxiosInstance = axios.create({
    baseURL: 'http://localhost:8080/'
  });

  if (withToken) {
    instance.interceptors.request.use(config => {
      const token: string | null = localStorage.getItem('token');
      if (token === null) {
        clearAuth();
        navigate('/login');
      }
      config.headers.Authorization = `Bearer ${token}`;
      return config;
    });
  }

  instance.interceptors.response.use((response: AxiosResponse<NulResponse<Response>>) => {
    return response;
  }, (error: AxiosError<NulResponse<Response>>) => {
    if (error.response) {
      const data: NulResponse<Response> = error.response.data;
      if (data.code === NulExceptionConstants.INVALID_TOKEN) {
        navigate('/login', { invalidToken: true });
      }
      if (errorHandlers !== undefined && data.code in errorHandlers) {
        errorHandlers[data.code](data.code, data.message);
      } else if (defaultHandler !== undefined) {
        defaultHandler(data.code, data.message);
      } else {
        fallbackHandler(data.code, data.message);
      }
      return Promise.reject(new NulException(data.code, data.message));
    } else {
      return Promise.reject(new NulException(NulExceptionConstants.NETWORK_ERROR, error.message));
    }
  });

  return instance;
}

interface HeaderRequestParams<Params> {
  url: string;
  params?: Params;
  errorHandlers?: ErrorHandlerMap;
  defaultHandler?: ErrorHandler;
  withToken?: boolean;
}

type RequestParams<Params, Request> = HeaderRequestParams<Params> & {
  request?: Request;
};

function postRequest<Response, Request, Params = null>(params: RequestParams<Params, Request>): Promise<AxiosResponse<NulResponse<Response>>> {
  const withToken: boolean = TypeHelper.getValueOrDefault(params.withToken, true);
  const instance: AxiosInstance = createAxiosInstance<Response>(withToken, params.errorHandlers, params.defaultHandler);
  return instance.post<NulResponse<Response>, AxiosResponse<NulResponse<Response>>, Request>(params.url, params.request, {
    params: params.params
  });
}

function getRequest<Response = null, Params = null>(params: HeaderRequestParams<Params>): Promise<AxiosResponse<NulResponse<Response>>> {
  const withToken: boolean = TypeHelper.getValueOrDefault(params.withToken, true);
  const instance: AxiosInstance = createAxiosInstance<Response>(withToken, params.errorHandlers, params.defaultHandler);
  return instance.get<NulResponse<Response>>(params.url, {
    params: params.params
  });
}

function putRequest<Response = null, Request = null, Params = null>(params: RequestParams<Params, Request>): Promise<AxiosResponse<NulResponse<Response>>> {
  const withToken: boolean = TypeHelper.getValueOrDefault(params.withToken, true);
  const instance: AxiosInstance = createAxiosInstance<Response>(withToken, params.errorHandlers, params.defaultHandler);
  return instance.put<NulResponse<Response>, AxiosResponse<NulResponse<Response>>, Request>(params.url, params.request, {
    params: params.params
  });
}

function deleteRequest<Response = null, Request = null, Params = null>(params: HeaderRequestParams<Params>): Promise<AxiosResponse<NulResponse<Response>>> {
  const withToken: boolean = TypeHelper.getValueOrDefault(params.withToken, true);
  const instance: AxiosInstance = createAxiosInstance<Response>(withToken, params.errorHandlers, params.defaultHandler);
  return instance.delete<NulResponse<Response>, AxiosResponse<NulResponse<Response>>, Request>(params.url, {
    params: params.params
  });
}

export { postRequest, getRequest, putRequest, deleteRequest };
export type { NulResponse, ErrorHandler, ErrorHandlerMap };